from cStringIO import StringIO
from datetime import datetime
from pyparsing import Literal, Or, Suppress, Word, hexnums, nums, restOfLine, \
    delimitedList

# A simple list and mapping of the short month names to their numerical values.
# This is used for validation when parsing log entries.
month_list = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
    'Oct', 'Nov', 'Dec']
monthmap = {}
for i in xrange(len(month_list)):
    monthmap[month_list[i]] = i + 1

# List of weekdays for log entry parsing validation.
wkday_list = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

def get_datetime(string, location, tokens):
    """Convert pyparsing.ParseResults to a datetime object and return it."""
    # Pull the various time values from the parsed tokens collection.
    year = int(tokens['entry_time_year'])
    month = monthmap[tokens['entry_time_month']]
    day = int(tokens['entry_time_day'])
    hrs, mins, secs = map(int, list(tokens['entry_time_time']))
    # Pack the values into a ``datetime`` instance and return it.
    return datetime.datetime(year, month, day, hrs, mins, secs)

# Begin definitions of BNF grammar for parsing log entries with pyparsing.
entry_logger_type = (Literal('CTRL') ^ Literal('ACTR')).setName(
    'entry_logger_type')

entry_logger_id = Word(hexnums, exact=32).setName('entry_logger_id')

entry_logger_spec = (entry_logger_type + Suppress('(') + entry_logger_id +
    Suppress(')')).setName('entry_logger_spec')

entry_level = Or(map(Literal, ['CRITICAL', 'ERROR', 'WARNING', 'INFO',
    'DEBUG', 'NOTSET'])).setName('entry_level')

entry_time_fmt = '%a, %d %b %Y %H:%M:%S'

entry_time_wkday = Or(map(Literal, wkday_list)).setName('entry_time_wkday')

entry_time_day = Word(nums, min=1, max=2).setName('entry_time_day')

entry_time_month = Or(map(Literal, month_list)).setName('entry_time_month')

entry_time_year = Word(nums, exact=4).setName('entry_time_year')

entry_time_time = delimitedList(Word(nums, exact=2), ':')

entry_time = (entry_time_wkday + Suppress(',') + entry_time_day + \
    entry_time_month + entry_time_year + entry_time_time).setName('entry_time')

entry_time = entry_time.setParseAction(get_datetime)

entry_message = restOfLine.setName('entry_message')

sep = Suppress('||')

line_parser = (entry_logger_spec + sep + entry_time + sep + entry_type + sep +
    entry_message).setName('entry')
# End BNF grammar definitions.

class Line(object):

    """
    Store variables about a line from a ClassAct log.
    
    The ``Line`` class, which extends the built-in ``object``, stores the data
    obtained from parsing a line in a log file generated by the functions in
    the ``classact.log`` module. By default, the parser used is the pyparsing
    parser held at ``classact.log_parse.line_parser``.
    
    An instance of ``Line`` contains the following attributes:
        
        ``line``
            A string containing the original line, as passed to ``__init__``.
            This can also be obtained by calling ``str(line_instance)``.
        
        ``logger_type``
            The object which originally wrote the log entry; this will either
            be 'CTRL' or 'ACTR' (corresponding to a controller or an actor).
        
        ``logger_id``
            The id of the object doing the logging. Whilst there is no
            distinction between the formats of the actor and controller ids,
            they are still UUIDs, and so there is an incredibly small
            possibility of a collision.
        
        ``level``
            The level of the log entry; one of 'CRITICAL', 'ERROR', 'WARNING',
            'INFO', 'DEBUG', or 'NOTSET'. For more information on levels,
            consult the documentation for the built-in ``logging`` module,
            which is included in the Python stdlib from version 2.3.
        
        ``time``
            A ``datetime`` instance, which contains the date and time at which
            the log entry was recorded. For more information on ``datetime``
            objects, consult the stdlib documentation for the ``datetime``
            module, which has been included in the stdlib since version 2.3.
        
        ``message``
            A description of the event, as created by the object doing the
            logging.
    
    The ``Line`` class also overrides the default ``__repr__`` and ``__str__``
    magic methods. More info can be found in their respective docstrings.
    
    """

    def __init__(self, line_str, line_parser=line_parser):
        """
        Initialize a ``Line`` instance, by parsing a line from a ClassAct log.
        
        This method accepts a string, containing the line, and a parser, and
        produces a ``Line`` instance which contains data from the line.
        
        The keyword argument ``line_parser`` is an object which, at its most
        basic level, provides a method ``parseString``, which accepts the log
        entry as a string and returns a dictionary of the results.
        
        The dictionary returned by ``line_parser.parseString`` should contain
        the following keys:
            
            ``entry_logger_type`` (string)
                The type of the object which carried out the logging. This is
                usually either 'CTRL' (for controller) or 'ACTR' (for actor).
                It should be stored as a string (not a boolean) so that the
                log format may be extended in future to include other logger
                types.
            
            ``entry_logger_id`` (string)
                The id of the object which carried out the logging; usually a
                string of hexadecimal values, of length 32.
            
            ``entry_level`` (string)
                The level of the log entry; one of 'CRITICAL', 'ERROR',
                'WARNING', 'INFO', 'DEBUG', or 'NOTSET'.
            
            ``entry_time`` (``datetime``)
                The date and time at which the log entry was created. This
                should be a ``datetime`` object; consult the documentation
                for the ``datetime`` module for more information on using this
                class.
            
            ``entry_message`` (string)
                The description of the log event, which is created by the
                object which carries out the logging.
        
        Consult the documentation in the ``classact.log`` module for
        information on the log format, and for parser design.
        
        """
        # Store original line in the ``Line`` instance. Allows for verification
        # of the parsing mechanism, ``repr``, and other uses.
        self.line = line_str
        parse_results = line_parser.parseString(line_str)
        # Get particular parts of the results and pack them into attributes in
        # the ``Line`` instance.
        self.logger_type = parse_results['entry_logger_type']
        self.logger_id = parse_results['entry_logger_id']
        self.level = parse_results['entry_level']
        self.time = parse_results['entry_time']
        self.message = parse_results['entry_message']
        object.__init__(self)

    def __repr__(self):
        """
        Return a representation of the ``Line`` instance.
        
        By default, this method returns a string which looks something like
        this:
            
            Line('CTRL(a7230e5dd6d64ed...')
            
        The first 20 characters of the original line are shown, with an
        ellipsis to show that the representation is truncated. This is because,
        with 128-bit ids, the length of lines begins to get very long indeed.
        
        """
        # This is where storing the original line string in the instance is
        # useful. The following conditional just makes sure that whatever's
        # printed is relatively short, in the interest of screen space.
        return 'Line(%r)' % (self.line[:20],)

    def __str__(self):
        """Return this line's original log entry."""
        return self.line

    def __cmp__(self, other_line):
        """Compare this ``Line`` instance to an arbitrary object.
        
        If the object is another ``Line`` instance, then return the
        comparison of their recorded times. This is helpful in sorting a list
        of ``Line`` instances (i.e. a ``Log`` instance) by time.
        
        If it is anything else, compare the time at which this instance was
        recorded to the other object itself. This can be used with lambda
        expressions to create filter functions, which in turn can be used to
        filter ``Log`` instances against cutoff date/time values. Just a
        suggestion.
        
        """
        # Check if the object to be compared is an instance of this instance's
        # parent class, as accessed through ``self.__class__``.
        if isinstance(other_line, self.__class__):
            # Return the comparison of the two ``Line`` instances' times.
            return cmp(self.time, other_line.time)
        else:
            return cmp(self.time, other_line)


class Log(list):

    """
    Store variables on the lines of a whole log.
    
    The ``Log`` class, which extends the built-in ``list`` datatype, is used
    to contain a collection of ``Line`` instances, and has several methods
    which add some useful functionality.
    
    The extra defined methods are:
        
        ``extend_handle``
            Extend the list of ``Line`` instances by parsing each line in a
            given file-like object. See the individual docstring for more
            information on the requirements for such an object.
        
        ``append_parse``
            Parses a given string and appends the resulting line object to the
            ``Log`` instance. See individual docstring for more info.
        
        ``filter``
            Works in a similar way to the built-in ``filter``: given a
            particular filtering function, create and return a new ``Log``
            instance which contains lines which 'pass' the filtering test;
            See the individual docstring for implementation details.
        
        ``__init__``
            Extends the default ``list.__init__`` method; simply populates the
            ``Log`` instance with some metadata. See the method's individual
            docstring for specifics.
        
        ``__cmp__``
            Causes the sorting and comparison of logs to default to a
            particular behaviour: if the object against which a log is being
            compared is another log, the comparison of their times is returned.
            Otherwise, the comparison of the log's time against the object
            itself is returned.
        
    A ``Log`` instance has several attributes defined, in addition to the
    common ``list`` attributes:
        
        ``line_class``
            This attribute holds the class which should be used to hold lines.
            Its ``__init__`` method should accept a string and a parser (in
            that order), and return a suitable object.
        
        ``line_parser``
            This attribute holds the parser instance which will be used to
            parse added lines. See the documentation for the ``Line`` class for
            more information on the requirements for a parser.
        
        ``name``
            An optional string attribute which holds the name of a particular
            log. It is not required, but useful for categorisation, etc. It is
            recommended to use something like a date and machine specifier
            (such as an IP address) for this attribute.
        
        ``time``
            A ``datetime`` instance which holds the date and time of the log
            file. This is useful in sorting logs.
    
    """

    def __init__(self, line_parser=line_parser, name='', time=datetime.now(),
        line_class=Line):
        """
        Initialize a log class with specific parameters.

        The ``Log.__init__`` method simply returns a ``Log`` instance where the
        attributes have been populated with the corresponding arguments as
        passed to the method. It extends the ``list.__init__`` method.

        There are no required arguments; the four accepted keyword arguments
        are as follows:

            ``line_parser``
                This should be a parser instance which conforms to the
                requirements for a parser as defined in the ``Line`` class
                documentation. This argument defaults to the
                ``line_parser`` instance, held in this module.

            ``name``
                This is an optional name for the ``Log`` instance, which
                defaults to an empty string.

            ``time``
                This should be a ``datetime`` object (see Python library
                reference for the ``datetime`` module) which represents the
                time and date of creation/modification of the log. This
                argument defaults to the date and time when the ``Log``
                instance was created, through calling ``datetime.now()``
                (that's on the ``datetime`` class, not the module).

            ``line_class``
                This argument, which defaults to the ``Line`` class (found in
                this module), should be a callable which, when called with a
                string and a parser instance as its first and second positional
                arguments respectively, should return a suitable object which
                represents a line. When called, the parser instance which is
                passed will be the ``Log`` instance's ``line_parser``
                attribute.

        All of these keyword arguments are mapped to the class attributes of
        the ``Log`` class; see the class's docstring for more information.
        
        """
        # Just store the arguments in the ``Log`` instance.
        self.line_parser = line_parser
        self.line_class = line_class
        self.name = name
        self.time = time
        # Required to initialize all list-specific attributes and methods.
        list.__init__(self)

    def append_parse(self, line):
        """Parse a line and append result to this ``Log`` instance."""
        # Not very complex, but useful nonetheless.
        self.append(self.line_class(line, self.line_parser))

    def extend_handle(self, handle):
        """
        Add the parsed lines from ``handle`` to this ``Log`` instance.
        
        The argument ``handle`` (the first and only positional argument) must
        have an ``__iter__`` magic method which returns an iterator. This
        iterator should, when its ``next`` method is called, return a line of
        input from the file. When there are no more lines of input, a
        ``StopIteration`` exception should be raised.
        
        Several file-like objects support this. ``file`` instances (as
        returned by ``open``) have a ``next`` method which is equivalent to
        ``file.readline``, and so ``file.__iter__`` simply returns the instance
        itself. A similar behavior is exhibited by the ``StringIO`` class (both
        the Python and the C implementations). The rationale behind using this
        is that it allows lists of strings to be parsed and appended without
        having to import a ``StringIO`` class; simply pass the list (or other
        iterable) as an argument to this method.
        
        This function parses and appends all lines in the file to the log by
        calling ``Log.append_parse`` on each line as it reads from the file.
        
        """
        handle_iter = iter(handle)
        # Typical while loop for use with iterators.
        while True:
            # Use the handle as an iterator, allowing for arguments other than
            # just file-like objects (i.e. generators).
            try:
                curr_line = handle_iter.next()
            except StopIteration:
                break
            # Use ``append_parse`` to add the line to this ``Log`` instance.
            self.append_parse(curr_line)

    def filter(self, filter_function):
        """
        Return a new ``Log`` instance with a filtered set of lines.
        
        This method filters the current ``Log`` instance in a similar way to
        the built-in ``filter`` function. It accepts a function which, when
        called with a line object as its first and only argument, returns a
        boolean value. If this value is ``True``, then the line is appended to
        the new ``Log`` instance. Otherwise, the line is not included in the
        returned ``Log`` instance.
        
        Because ``Log`` inherits from ``list``, this method uses the built-in
        ``filter`` to retrieve a list of line instances. A new ``Log`` instance
        is then initialized with the current instance's attributes, and this
        new instance is then populated with the filtered list of lines and
        returned.
        
        """
        # Use built-in filter to get a new list of lines.
        new_lines = filter(filter_function, list(self))
        # Construct a new log using the attributes of this one.
        new_log = self.__class__(line_parser=self.line_parser, name=self.name,
            time=self.time, line_class=self.line_class)
        # Populate new log with filtered lines, and return to caller.
        new_log.extend(new_lines)
        return new_log

    def __cmp__(self, other_log):
        """
        Compare a ``Log`` instance to another object.
        
        If the other object is another ``Log`` instance, then the comparison
        of their ``time`` attributes is returned.
        
        Otherwise, the comparison of the ``Log`` instance's time and the other
        object itself is returned.
        
        """
        if isinstance(other_log, self.__class__):
            return cmp(self.time, other_log.time)
        else:
            return cmp(self.time, other_log)